﻿using System;
using System.Text;

using BizElements.Core;
using PostSharp.Aspects;

using BizElements.BusinessLayer;

namespace BizElements.BusinessLayer.Aspects
{
    /// <summary>
    /// Used by <see cref="CachedAttribute"/> to generate unique cache keys based on class name, method name and method arguments.
    /// </summary>
    /// <remarks>Suitable for projects which use <b>BizElements</b> generated code.</remarks>
    [Serializable]
    public sealed class CacheKeyGenerator : ICacheKeyGenerator
    {
        /// <summary>
        /// Gets the first occurence of <b>IActor</b> object from the methods arguments.
        /// </summary>
        /// <param name="eventArgs">Event arguments specifying which method is being executed and which are its arguments.</param>
        /// <returns><b>IActor</b> object or <b>null</b> if user cannot be extracted from method arguments.</returns>
        public IActor GetActor(MethodExecutionArgs eventArgs)
        {
            return AopUtil.GetActor(eventArgs);
        }

        /// <summary>
        /// Generates cache key for the method's return value.
        /// </summary>
        /// <param name="eventArgs">Event arguments specifying which method is being executed and which are its arguments.</param>
        /// <param name="includeActor">Whether to include actor in the cache key. Should be set to <b>false</b> when using <see cref="UserCache"/> 
        /// or if actor is not used to restrict data which is returned.</param>
        /// <param name="mutableParamsAllowed">Whether cache key can be generated for mutable objects.</param>
        /// <returns>A string which represents a unique cache key.</returns>
        public string GetCacheKey(MethodExecutionArgs eventArgs, bool includeActor, bool mutableParamsAllowed)
        {
            StringBuilder key = new StringBuilder();
            AppendMethodPath(key, eventArgs);
            IActor user = GetActor(eventArgs);
            object[] arguments = eventArgs.Arguments.ToArray() ?? new object[0];
            foreach (object arg in arguments)
            {
                bool skipActor = (!includeActor) && (arg == user);
                if (!skipActor)
                {
                    key.Append(":");
                    bool noErrors = AppendArgumentHashToCacheKey(key, mutableParamsAllowed, arg);
                    if (!noErrors)
                        return null;
                }
            }

            return key.ToString();
        }

        private static void AppendMethodPath(StringBuilder key, MethodExecutionArgs eventArgs)
        {
            key.Append(eventArgs.Method.DeclaringType.FullName);
            key.Append('.');
            key.Append(eventArgs.Method.Name);
        }

        private static bool AppendArgumentHashToCacheKey(StringBuilder key, bool mutableParamsAllowed, object arg)
        {
            if (arg == null)
                // No errors.
                return true;

            if (mutableParamsAllowed)
            {
                if (arg is IEntity)
                    AppendEntityHashToCacheKey(key, (IEntity)arg);
                else
                    key.Append(arg.GetHashCode());
            }
            else
            {
                if (IsImutableType(arg.GetType()))
                    key.Append(arg.GetHashCode());
                else
                    // Error. Should not cache. Cannot create unique key that will stick. 
                    // Equal object with same data might generate a different key which may lead to memory leak,
                    // ie. caching same data many times.
                    return false;
            }

            // No errors.
            return true;
        }

        private static void AppendEntityHashToCacheKey(StringBuilder key, IEntity entity)
        {
            foreach (object fieldValue in entity.ToObjectArray())
            {
                if (fieldValue != null)
                    key.Append(fieldValue.GetHashCode());

                key.Append("#");
            }
        }

        private static bool IsImutableType(Type t)
        {
            if (t.IsValueType)
                return true;
            else if (t == typeof(string))
                return true;
            else
                return false;
        }
    }
}